﻿using AutoMapper;
using MongoDB.Driver;
using System.Linq.Expressions;
using System.Reflection;
using Testudo.Core;
using Testudo.Core.Attributes;
using Testudo.Core.ViewModels;

namespace Testudo.Mongo;

public class MongoRepository<TEntity> : IRepository<TEntity>
    where TEntity : Entity, IEntity
{
    public MongoRepository(IMongoDatabase mongoDb)
    {
        MongoDb = mongoDb;
    }

    private IMongoDatabase MongoDb { get; }


    protected IMongoCollection<TEntity> DataCollection =>
        MongoDb.GetCollection<TEntity>(GetCollectionName());

    protected IMongoCollection<ChangeTrackRecord> ChangeTrackDataCollection =>
        MongoDb.GetCollection<ChangeTrackRecord>($"{GetCollectionName()}_ChangeTrackRecord");

    public async Task<bool> AnyAsync(Expression<Func<TEntity, bool>> filter = null)
    {
        return await DataCollection.CountDocumentsAsync(EnsureFilter(filter)) > 0;
    }

    public async Task<List<TEntity>> GetListAsync(Expression<Func<TEntity, bool>> filter = null)
    {
        return (await DataCollection.FindAsync(EnsureFilter(filter))).ToList();
    }

    public async Task<PagingList<TEntity>> PagingListAsync(Expression<Func<TEntity, bool>> filter = null,
        int pageIndex = 1, int pageSize = 10)
    {
        return await DataCollection.PagingListAsync(EnsureFilter(filter), pageIndex, pageSize);
    }

    public virtual TEntity Create(TEntity entity)
    {
        DataCollection.InsertOne(entity);
        TryTrackChanges(entity, RecordChangeType.Create);
        return entity;
    }

    public virtual async Task<TEntity> CreateAsync(TEntity target)
    {
        await DataCollection.InsertOneAsync(target);
        await TryTrackChangesAsync(target, RecordChangeType.Create);
        return target;
    }

    public TEntity Update(TEntity entity)
    {
        var target = Get(entity.Id);

        if (target is IVersionEntity versionTarget && entity is IVersionEntity versionEntity)
            if (versionTarget.Version > versionEntity.Version)
                throw new EntityVersionErrorException();

        var props = typeof(TEntity).GetProperties(BindingFlags.Public | BindingFlags.Instance);
        var updateTarget = Builders<TEntity>.Update.Set(a => a.Id, entity.Id);
        foreach (var prop in props) updateTarget = updateTarget.Set(prop.Name, prop.GetValue(entity));
        if (entity is IVersionEntity tmp) updateTarget = updateTarget.Set(nameof(tmp.Version), tmp.Version + 1);
        var result = DataCollection.UpdateOne(a => a.Id == entity.Id, updateTarget);
        return result.MatchedCount > 0 ? entity : default;
    }

    public async Task<TEntity> UpdateAsync(TEntity entity)
    {
        var target = await GetAsync(entity.Id);

        if (target is IVersionEntity versionTarget && entity is IVersionEntity versionEntity)
            if (versionTarget.Version > versionEntity.Version)
                throw new EntityVersionErrorException();

        var (records, updateTarget) = target.UpdateWithValue(entity);
        if (records.Count > 0)
        {
            if (entity is IVersionEntity tmp) updateTarget = updateTarget.Set(nameof(tmp.Version), tmp.Version + 1);

            var result = await DataCollection.UpdateOneAsync(a => a.Id == entity.Id, updateTarget);

            await TryTrackChangesAsync(new ChangeTrackRecord
            {
                CreatedOn = DateTime.Now,
                TargetId = entity.Id,
                ChangeType = RecordChangeType.Update,
                Fields = records
            });
            return result.MatchedCount > 0 ? entity : default;
        }

        return entity;
    }

    public async Task<TEntity> UpdateAsync(Guid id, IDictionary<string, object> data)
    {
        var entity = await GetAsync(id);
        if (entity == null) return default;

        var (records, updateTarget) = entity.UpdateWithValue(data);
        var result = await DataCollection.UpdateOneAsync(a => a.Id == id, updateTarget);
        await TryTrackChangesAsync(new ChangeTrackRecord
        {
            CreatedOn = DateTime.Now,
            TargetId = entity.Id,
            ChangeType = RecordChangeType.Update,
            Fields = records
        });
        return result.MatchedCount > 0 ? await GetAsync(id) : default;
    }

    public async Task<TEntity> PatchAsync(PatchData patch)
    {
        var entity = await GetAsync(patch.Id);
        if (entity == null) return default;

        var (records, updateTarget) = entity.UpdateWithValue(patch);
        var result = await DataCollection.UpdateOneAsync(a => a.Id == patch.Id, updateTarget);
        await TryTrackChangesAsync(new ChangeTrackRecord
        {
            CreatedOn = DateTime.Now,
            TargetId = entity.Id,
            ChangeType = RecordChangeType.Patch,
            Fields = records
        });
        return result.MatchedCount > 0 ? entity : default;
    }

    public async Task<TEntity> SlashAsync(SlashData data)
    {
        var entity = await GetAsync(data.Id);
        if (entity == null) return default;

        var (records, updateTarget) = entity.Slash(data);
        var result = await DataCollection.UpdateOneAsync(a => a.Id == data.Id, updateTarget);
        await TryTrackChangesAsync(new ChangeTrackRecord
        {
            CreatedOn = DateTime.Now,
            TargetId = entity.Id,
            ChangeType = RecordChangeType.Slash,
            Fields = records
        });
        return result.MatchedCount > 0 ? entity : default;
    }

    public bool Delete(Guid id)
    {
        var item = Get(id);
        if (item != null) return DataCollection.DeleteOne(a => a.Id == item.Id).DeletedCount > 0;

        return false;
    }

    public async Task<bool> DeleteAsync(Guid id)
    {
        var item = await GetAsync(id);
        if (item != null)
        {
            var result = await DataCollection.DeleteOneAsync(a => a.Id == item.Id);
            await TryTrackChangesAsync(new ChangeTrackRecord
            {
                TargetId = id,
                ChangeType = RecordChangeType.Delete
            });
            return result.DeletedCount > 0;
        }

        return false;
    }

    public async Task<bool> DeleteAsync(Expression<Func<TEntity, bool>> filter)
    {
        var itemIds = (await GetListAsync(filter)).Select(a => a.Id).ToList();
        var result = await DataCollection.DeleteManyAsync(EnsureFilter(filter));
        if (itemIds.Any())
            foreach (var itemId in itemIds)
                await TryTrackChangesAsync(new ChangeTrackRecord
                {
                    TargetId = itemId,
                    ChangeType = RecordChangeType.Delete
                });

        return result.DeletedCount > 0;
    }

    public long Count(Expression<Func<TEntity, bool>> filter = null)
    {
        return DataCollection.CountDocuments(EnsureFilter(filter));
    }

    public async Task<long> CountAsync(Expression<Func<TEntity, bool>> filter = null)
    {
        return await DataCollection.CountDocumentsAsync(EnsureFilter(filter));
    }

    public TEntity FirstOrDefault(Expression<Func<TEntity, bool>> filter = null)
    {
        return DataCollection.Find(EnsureFilter(filter)).FirstOrDefault();
    }

    public async Task<TEntity> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> filter = null)
    {
        return (await DataCollection.FindAsync(EnsureFilter(filter))).FirstOrDefault();
    }

    public TEntity Get(Guid id)
    {
        Expression<Func<TEntity, bool>> filter = a => a.Id == id;
        return DataCollection.Find(EnsureFilter(filter)).FirstOrDefault();
    }

    public async Task<TEntity> GetAsync(Guid id)
    {
        Expression<Func<TEntity, bool>> filter = a => a.Id == id;
        return (await DataCollection.FindAsync(EnsureFilter(filter))).FirstOrDefault();
    }


    protected virtual string GetCollectionName()
    {
        return typeof(TEntity).Name;
    }

    protected virtual Expression<Func<TEntity, bool>> EnsureFilter(Expression<Func<TEntity, bool>> filter,
        Expression<Func<TEntity, bool>> defaultFilter = null)
    {
        return filter ?? defaultFilter ?? (e => true);
    }

    #region TryTrackChanges

    private void TryTrackChanges(TEntity entity, RecordChangeType type)
    {
        var records = entity.GetFields();

        TryTrackChanges(new ChangeTrackRecord
        {
            TargetId = entity.Id,
            ChangeType = type,
            Fields = records
        });
    }

    private async Task TryTrackChangesAsync(TEntity entity, RecordChangeType type)
    {
        var records = entity.GetFields();

        await TryTrackChangesAsync(new ChangeTrackRecord
        {
            CreatedOn = DateTime.Now,
            TargetId = entity.Id,
            ChangeType = type,
            Fields = records
        });
    }

    private void TryTrackChanges(ChangeTrackRecord record)
    {
        if (typeof(TEntity).GetCustomAttribute<ChangeTrackAttribute>() == null) return;
        ChangeTrackDataCollection.InsertOne(record);
    }

    protected async Task TryTrackChangesAsync(ChangeTrackRecord record)
    {
        if (typeof(TEntity).GetCustomAttribute<ChangeTrackAttribute>() == null) return;

        await ChangeTrackDataCollection.InsertOneAsync(record);
    }

    #endregion
}

internal class MongoRepository<TEntity, TVM, TCreateVM, TUpdateVM> : MongoRepository<TEntity>,
    IRepository<TEntity, TVM, TCreateVM, TUpdateVM>
    where TEntity : Entity, IEntity
    where TCreateVM : CreateVM
    where TUpdateVM : UpdateVM
    where TVM : class, IVMBase
{
    protected IMapper Mapper { get; }

    public MongoRepository(IMongoDatabase mongoDb, IMapper mapper) : base(mongoDb)
    {
        Mapper = mapper;
    }

    async Task<TVM> IRepository<TEntity, TVM, TCreateVM, TUpdateVM>.CreateAsync(TCreateVM vm)
    {
        var item = Mapper.Map<TEntity>(vm);
        item = await CreateAsync(item);

        return Map<TEntity, TVM>(item);
    }

    async Task<TVM> IRepository<TEntity, TVM, TCreateVM, TUpdateVM>.UpdateAsync(TUpdateVM vm)
    {
        var props = typeof(TUpdateVM).GetProperties(BindingFlags.Public | BindingFlags.Instance |
                                                    BindingFlags.DeclaredOnly);

        var dic = props.ToDictionary(prop => prop.Name, prop => prop.GetValue(vm));
        var item = await UpdateAsync(vm.Id, dic);

        return Map<TEntity, TVM>(item);
    }

    async Task<TVM> IRepository<TEntity, TVM, TCreateVM, TUpdateVM>.UpdateAsync(Guid id,
        IDictionary<string, object> data)
    {
        var item = await GetAsync(id);
        if (item == null) return default;

        var (records, updateTarget) = item.UpdateWithValue(data);
        var result = await DataCollection.UpdateOneAsync(a => a.Id == id, updateTarget);
        await TryTrackChangesAsync(new ChangeTrackRecord
        {
            CreatedOn = DateTime.Now,
            TargetId = item.Id,
            ChangeType = RecordChangeType.Update,
            Fields = records
        });
        return result.MatchedCount > 0 ? Mapper.Map<TVM>(item) : default;
    }

    TVM IRepository<TEntity, TVM, TCreateVM, TUpdateVM>.Get(Guid id)
    {
        var item = Get(id);

        return Map<TEntity, TVM>(item);
    }

    async Task<TVM> IRepository<TEntity, TVM, TCreateVM, TUpdateVM>.GetAsync(Guid id)
    {
        var item = await GetAsync(id);
        return Map<TEntity, TVM>(item);
    }

    async Task<TVM> IRepository<TEntity, TVM, TCreateVM, TUpdateVM>.PatchAsync(PatchData patch)
    {
        var item = await PatchAsync(patch);
        return Map<TEntity, TVM>(item);
    }

    async Task<TVM> IRepository<TEntity, TVM, TCreateVM, TUpdateVM>.SlashAsync(SlashData data)
    {
        var item = await SlashAsync(data);
        return Map<TEntity, TVM>(item);
    }

    async Task<TVM> IRepository<TEntity, TVM, TCreateVM, TUpdateVM>.FirstOrDefaultAsync(
        Expression<Func<TEntity, bool>> filter)
    {
        var item = await FirstOrDefaultAsync(filter);
        return Map<TEntity, TVM>(item);
    }

    async Task<TVM> IRepository<TEntity, TVM, TCreateVM, TUpdateVM>.CreateAsync(TEntity target)
    {
        var item = await CreateAsync(target);

        return Map<TEntity, TVM>(item);
    }

    async Task<List<TVM>> IRepository<TEntity, TVM, TCreateVM, TUpdateVM>.GetListAsync(
        Expression<Func<TEntity, bool>> filter)
    {
        var items = await GetListAsync(EnsureFilter(filter));
        return items.Select(Mapper.Map<TVM>).ToList();
    }

    async Task<PagingList<TVM>> IRepository<TEntity, TVM, TCreateVM, TUpdateVM>.PagingListAsync(
        Expression<Func<TEntity, bool>> filter, int pageIndex, int pageSize)
    {
        var result = await PagingListAsync(filter, pageIndex, pageSize);
        return result.PagingListMap<TEntity, TVM>(Mapper);
    }

    async Task<TVM> IRepository<TEntity, TVM, TCreateVM, TUpdateVM>.UpdateAsync(TEntity target)
    {
        var item = await UpdateAsync(target);
        return Map<TEntity, TVM>(item);
    }

    private TTarget Map<TSource, TTarget>(TSource source)
    {
        return source != null ? Mapper.Map<TSource, TTarget>(source) : default;
    }
}